package com.ngds.library.downloadcenter;

import android.content.ContentValues;
import android.content.Context;
import android.net.http.AndroidHttpClient;
import android.os.PowerManager;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;

import com.product.android.utils.LogUtils;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;

/**
 * Runs an actual download
 */
public class DownloadThread extends Thread {

	private final Context mContext;
	private final AppDownloadInfo mInfo;
	private final SystemFacade mSystemFacade;
	private final DownloadStorageManager mStorageManager;

	private volatile boolean mPolicyDirty;

	public DownloadThread(Context context, SystemFacade systemFacade, AppDownloadInfo info,
			DownloadStorageManager storageManager) {
		mContext = context;
		mSystemFacade = systemFacade;
		mInfo = info;
		mStorageManager = storageManager;
	}

	/**
	 * Returns the user agent provided by the initiating app, or use the default one
	 */
	private String userAgent() {
		String userAgent = mInfo.mUserAgent;
		if (userAgent != null) {
		}
		if (userAgent == null) {
			userAgent = Constants.DEFAULT_USER_AGENT;
		}
		return userAgent;
	}

	/**
	 * State for the entire run() method.
	 */
	static class State {
		public String mFilename;
		public FileOutputStream mStream;
		public String mMimeType;
		public boolean mCountRetry = false;
		public int mRetryAfter = 0;
		public int mRedirectCount = 0;
		public String mNewUri;
		public boolean mGotData = false;
		public String mRequestUri;
		public long mTotalBytes = -1;
		public long mCurrentBytes = 0;
		public String mHeaderETag;
		public boolean mContinuingDownload = false;
		public long mBytesNotified = 0;
		public long mTimeLastNotification = 0;

		public State(AppDownloadInfo info) {
			mMimeType = sanitizeMimeType(info.mMimeType);
			mRequestUri = info.mUri;
			mFilename = info.mFileName;
			mTotalBytes = info.mTotalBytes;
			mCurrentBytes = info.mCurrentBytes;
		}
	}


	/**
	 * State within executeDownload()
	 */
	private static class InnerState {
		public String mHeaderContentLength;
		public String mHeaderContentDisposition;
		public String mHeaderContentLocation;
	}


	/**
	 * Raised from methods called by executeDownload() to indicate that the download should be
	 * retried immediately.
	 */
	private class RetryDownload extends Throwable {
	}


	/**
	 * Executes the download in a separate thread
	 */
	@Override
	public void run() {
		Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

		State state = new State(mInfo);
		AndroidHttpClient client = null;
		PowerManager.WakeLock wakeLock = null;
		int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
		String errorMsg = null;

		//   final NetworkPolicyManager netPolicy = NetworkPolicyManager.getSystemService(mContext);
		final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);

		try {
			wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
			wakeLock.acquire();

			// while performing download, register for rules updates
			// netPolicy.registerListener(mPolicyListener);

			if (Constants.LOGV) {
				Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
			}

			client = AndroidHttpClient.newInstance(userAgent(), mContext);

			// network traffic on this thread should be counted against the
			// requesting uid, and is tagged with well-known value.

			//    TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
			//     TrafficStats.setThreadStatsUid(mInfo.mUid);

			boolean finished = false;
			while (!finished) {
				Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
				// Set or unset proxy, which may have changed since last GET request.
				// setDefaultProxy() supports null as proxy parameter.


				//                ConnRouteParams.setDefaultProxy(client.getParams(),
				//                        Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
				HttpGet request = new HttpGet(state.mRequestUri);
				try {
					executeDownload(state, client, request);
					finished = true;
				} catch (RetryDownload exc) {
					// fall through
				} finally {
					request.abort();
					request = null;
				}
			}

			if (Constants.LOGV) {
				Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
			}
			finalizeDestinationFile(state);
			finalStatus = Downloads.Impl.STATUS_SUCCESS;
		} catch (DownloadStopedException error) {
			//在这里处理各种失败原因
			// remove the cause before printing, in case it contains PII
			errorMsg = error.getMessage();
			String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
			Log.w(Constants.TAG, msg);
			if (Constants.LOGV) {
				Log.w(Constants.TAG, msg, error);
			}
			finalStatus = error.mFinalStatus;
			//假如剩下空间不足则删除该下载
			if (error.mFinalStatus == Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR) {
				//                AppDownloadManager.getDownloadManagerService(mContext).remove(mInfo.mId);
			}
			LogUtils.e("DownloadThread.java -- jiangwb", msg);
			// fall through to finally block
		} catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
			errorMsg = ex.getMessage();
			String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
			Log.w(Constants.TAG, msg, ex);
			finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
			// falls through to the code that reports an error
		} finally {
			//            TrafficStats.clearThreadStatsTag();

			if (client != null) {
				client.close();
				client = null;
			}
			cleanupDestination(state, finalStatus);
			notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
					state.mGotData, state.mFilename,
					state.mNewUri, state.mMimeType, errorMsg);
			DownloadHandler.getInstance().dequeueDownload(mInfo.mId);


			if (wakeLock != null) {
				wakeLock.release();
				wakeLock = null;
			}
		}
		mStorageManager.incrementNumDownloadsSoFar();
	}

	/**
	 * Fully execute a single download request - setup and send the request, handle the response,
	 * and transfer the data to the destination file.
	 */
	private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
			throws DownloadStopedException, RetryDownload {
		InnerState innerState = new InnerState();
		byte data[] = new byte[Constants.BUFFER_SIZE];

		setupDestinationFile(state, innerState);
		addRequestHeaders(state, request);

		// skip when already finished; remove after fixing race in 5217390
		if (state.mCurrentBytes == state.mTotalBytes) {
			Log.i(Constants.TAG, "Skipping initiating request for download " +
					mInfo.mId + "; already completed");
			return;
		}

		// check just before sending the request to avoid using an invalid connection at all
		checkConnectivity();

		HttpResponse response = sendRequest(state, client, request);
		handleExceptionalStatus(state, innerState, response);

		if (Constants.LOGV) {
			Log.v(Constants.TAG, "received response for " + mInfo.mUri);
		}

		processResponseHeaders(state, innerState, response);
		InputStream entityStream = openResponseEntity(state, response);
		transferData(state, innerState, data, entityStream);
	}

	/**
	 * Check if current connectivity is valid for this request.
	 */
	private void checkConnectivity() throws DownloadStopedException {
		// checking connectivity will apply current policy
		mPolicyDirty = false;

		int networkUsable = mInfo.checkCanUseNetwork();
		if (networkUsable != AppDownloadInfo.NETWORK_OK) {
			int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
			if (networkUsable == AppDownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
				status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
				mInfo.notifyPauseDueToSize(true);
			} else if (networkUsable == AppDownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
				status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
				mInfo.notifyPauseDueToSize(false);
			} else if (networkUsable == AppDownloadInfo.NETWORK_BLOCKED) {
				status = Downloads.Impl.STATUS_BLOCKED;
			}
			throw new DownloadStopedException(status,
					mInfo.getLogMessageForNetworkError(networkUsable));
		}
	}

	/**
	 * Transfer as much data as possible from the HTTP response to the destination file.
	 *
	 * @param data         buffer to use to read data
	 * @param entityStream stream for reading the HTTP response entity
	 */
	private void transferData(
			State state, InnerState innerState, byte[] data, InputStream entityStream)
					throws DownloadStopedException {
		for (; ; ) {
			int bytesRead = readFromResponse(state, innerState, data, entityStream);
			if (bytesRead == -1) { // success, end of stream already reached
				handleEndOfStream(state, innerState);
				return;
			}

			state.mGotData = true;
			writeDataToDestination(state, data, bytesRead);
			state.mCurrentBytes += bytesRead;
			reportProgress(state, innerState);

			//edit by lix 在使用中有发现已下载字节大于总大小以及下载无法结束的情况
			/*if (state.mCurrentBytes > state.mTotalBytes) {
                throw new DownloadStopedException(Downloads.Impl.STATUS_CANNOT_RESUME,
                        Constants.Tip.MISMATCH_CONTENT_LENGTH);
            }*/

			if (Constants.LOGVV) {
				Log.v(Constants.TAG, "downloaded " + state.mCurrentBytes + " for "
						+ mInfo.mUri);
			}

			checkPausedOrCanceled(state);
		}
	}

	/**
	 * Called after a successful completion to take any necessary action on the downloaded file.
	 */
	private void finalizeDestinationFile(State state) throws DownloadStopedException {
		if (state.mFilename != null) {
			// make sure the file is readable
			//  FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
			syncDestination(state);
		}
	}

	/**
	 * Called just before the thread finishes, regardless of status, to take any necessary action on
	 * the downloaded file.
	 */
	private void cleanupDestination(State state, int finalStatus) {
		//        if (mDrmConvertSession != null) {
		//            finalStatus = mDrmConvertSession.close(state.mFilename);
		//        }

		closeDestination(state);
		if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
			new File(state.mFilename).delete();
			state.mFilename = null;
		}
	}

	/**
	 * Sync the destination file to storage.
	 */
	private void syncDestination(State state) {
		FileOutputStream downloadedFileStream = null;
		try {
			downloadedFileStream = new FileOutputStream(state.mFilename, true);
			downloadedFileStream.getFD().sync();
		} catch (FileNotFoundException ex) {
			Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
		} catch (SyncFailedException ex) {
			Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
		} catch (IOException ex) {
			Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
		} catch (RuntimeException ex) {
			Log.w(Constants.TAG, "exception while syncing file: ", ex);
		} finally {
			if (downloadedFileStream != null) {
				try {
					downloadedFileStream.close();
				} catch (IOException ex) {
					Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
				} catch (RuntimeException ex) {
					Log.w(Constants.TAG, "exception while closing file: ", ex);
				}
			}
		}
	}

	/**
	 * Close the destination output stream.
	 */
	private void closeDestination(State state) {
		try {
			// close the file
			if (state.mStream != null) {
				state.mStream.close();
				state.mStream = null;
			}
		} catch (IOException ex) {
			if (Constants.LOGV) {
				Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
			}
			// nothing can really be done if the file can't be closed
		}
	}

	/**
	 * Check if the download has been paused or canceled, stopping the request appropriately if it
	 * has been.
	 */
	private void checkPausedOrCanceled(State state) throws DownloadStopedException {
		synchronized (mInfo) {
			if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
				throw new DownloadStopedException(
						Downloads.Impl.STATUS_PAUSED_BY_APP, Constants.ERROR_MSG.PAUSE_BY_OWNER);
			}
			if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
				throw new DownloadStopedException(Downloads.Impl.STATUS_CANCELED,
						Constants.ERROR_MSG.CANCELED);
			}
		}

		// if policy has been changed, trigger connectivity check
		if (mPolicyDirty) {
			checkConnectivity();
		}
	}

	/**
	 * Report download progress through the database if necessary.
	 */
	private void reportProgress(State state, InnerState innerState) {
		long now = mSystemFacade.currentTimeMillis();
		if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
				now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
			ContentValues values = new ContentValues();
			values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
			mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
			state.mBytesNotified = state.mCurrentBytes;
			state.mTimeLastNotification = now;
		}
	}

	/**
	 * Write a data buffer to the destination file.
	 *
	 * @param data      buffer containing the data to write
	 * @param bytesRead how many bytes to write from the buffer
	 */
	private void writeDataToDestination(State state, byte[] data, int bytesRead)
			throws DownloadStopedException {
		for (; ; ) {
			try {
				if (state.mStream == null) {
					state.mStream = new FileOutputStream(state.mFilename, true);
				}
				mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
						bytesRead);
				state.mStream.write(data, 0, bytesRead);

				return;
			} catch (IOException ex) {
				// couldn't write to file. are we out of space? check.
						// TODO this check should only be done once. why is this being done
				// in a while(true) loop (see the enclosing statement: for(;;)
				if (state.mStream != null) {
					mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
				}
			} finally {
				if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
					closeDestination(state);
				}
			}
		}
	}

	/**
	 * Called when we've reached the end of the HTTP response stream, to update the database and
	 * check for consistency.
	 */
	private void handleEndOfStream(State state, InnerState innerState)
			throws DownloadStopedException {
		ContentValues values = new ContentValues();
		values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
		if (innerState.mHeaderContentLength == null) {
			values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, state.mCurrentBytes);
		}
		mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);

		boolean lengthMismatched = (innerState.mHeaderContentLength != null)
				&& (state.mCurrentBytes != Integer.parseInt(innerState.mHeaderContentLength));
		if (lengthMismatched) {
			if (cannotResume(state)) {
				throw new DownloadStopedException(Downloads.Impl.STATUS_CANNOT_RESUME,
						Constants.ERROR_MSG.MISMATCH_CONTENT_LENGTH);//无法匹配内容大小
			} else {
				throw new DownloadStopedException(getFinalStatusForHttpError(state),
						Constants.ERROR_MSG.SOCEKET_CLOSED);//文件输出被提前结束
			}
		}
	}

	private boolean cannotResume(State state) {
		return state.mCurrentBytes > 0 && !mInfo.mNoIntegrity && state.mHeaderETag == null;
	}

	/**
	 * Read some data from the HTTP response stream, handling I/O errors.
	 *
	 * @param data         buffer to use to read data
	 * @param entityStream stream for reading the HTTP response entity
	 * @return the number of bytes actually read or -1 if the end of the stream has been reached
	 */
	private int readFromResponse(State state, InnerState innerState, byte[] data,
			InputStream entityStream) throws DownloadStopedException {
		try {
			return entityStream.read(data);
		} catch (IOException ex) {
			logNetworkState();
			ContentValues values = new ContentValues();
			values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
			mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
			//            if (cannotResume(state)) {
				//                String message = "while reading response: " + ex.toString()
			//                + ", can't resume interrupted download with no ETag";
			//                throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
			//                        message, ex);
			//            } else {
			String errorMsg = Helpers.isNetworkAvailable(mSystemFacade) ?
					Constants.ERROR_MSG.HTTP_ERROR :
						Constants.ERROR_MSG.HTTP_NONE_NET;
			throw new DownloadStopedException(getFinalStatusForHttpError(state),
					errorMsg, ex);
			//            }
		}
	}

	/**
	 * Open a stream for the HTTP response entity, handling I/O errors.
	 *
	 * @return an InputStream to read the response entity
	 */
	private InputStream openResponseEntity(State state, HttpResponse response)
			throws DownloadStopedException {
		try {
			return response.getEntity().getContent();
		} catch (IOException ex) {
			logNetworkState();
			throw new DownloadStopedException(getFinalStatusForHttpError(state),
					"while getting entity: " + ex.toString(), ex);
		}
	}

	private void logNetworkState() {
		if (Constants.LOGX) {
			Log.i(Constants.TAG,
					"Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
		}
	}

	/**
	 * Read HTTP response headers and take appropriate action, including setting up the destination
	 * file and updating the database.
	 */
	private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
			throws DownloadStopedException {
		if (state.mContinuingDownload) {
			// ignore response headers on resume requests
			return;
		}

		readResponseHeaders(state, innerState, response);

		state.mFilename = Helpers.generateSaveFile(
				mContext,
				mInfo.mUri,
				mInfo.mHint,
				innerState.mHeaderContentDisposition,
				innerState.mHeaderContentLocation,
				state.mMimeType,
				mInfo.mDestination,
				(innerState.mHeaderContentLength != null) ?
						Long.parseLong(innerState.mHeaderContentLength) : 0,
						mInfo.mIsPublicApi, mStorageManager
				);
		try {
			state.mStream = new FileOutputStream(state.mFilename);
		} catch (FileNotFoundException exc) {
			exc.printStackTrace();
			new File(state.mFilename);
			try {
				state.mStream = new FileOutputStream(state.mFilename);
			} catch (FileNotFoundException e) {
				throw new DownloadStopedException(Downloads.Impl.STATUS_FILE_ERROR,
						Constants.ERROR_MSG.FILE_NOT_EXIT, exc);
			}

			//changed
			//            throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
					//                    "while opening destination file: " + exc.toString(), exc);
			//changed
		}
		if (Constants.LOGV) {
			Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
		}

		updateDatabaseFromHeaders(state, innerState);
		// check connectivity again now that we know the total size
		checkConnectivity();
	}

	/**
	 * Update necessary database fields based on values of HTTP response headers that have been
	 * read.
	 */
	private void updateDatabaseFromHeaders(State state, InnerState innerState) {
		ContentValues values = new ContentValues();
		values.put(Downloads.Impl._DATA, state.mFilename);
		if (state.mHeaderETag != null) {
			values.put(Constants.ETAG, state.mHeaderETag);
		}
		if (state.mMimeType != null) {
			values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
		}
		values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
		mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
	}

	/**
	 * Read headers from the HTTP response and store them into local state.
	 */
	private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
			throws DownloadStopedException {
		Header header = response.getFirstHeader("Content-Disposition");
		if (header != null) {
			innerState.mHeaderContentDisposition = header.getValue();
		}
		header = response.getFirstHeader("Content-Location");
		if (header != null) {
			innerState.mHeaderContentLocation = header.getValue();
		}
		if (state.mMimeType == null) {
			header = response.getFirstHeader("Content-Type");
			if (header != null) {
				state.mMimeType = sanitizeMimeType(header.getValue());
			}
		}
		header = response.getFirstHeader("ETag");
		if (header != null) {
			state.mHeaderETag = header.getValue();
		}
		String headerTransferEncoding = null;
		header = response.getFirstHeader("Transfer-Encoding");
		if (header != null) {
			headerTransferEncoding = header.getValue();
		}
		if (headerTransferEncoding == null) {
			header = response.getFirstHeader("Content-Length");
			if (header != null) {
				innerState.mHeaderContentLength = header.getValue();
				state.mTotalBytes = mInfo.mTotalBytes =
						Long.parseLong(innerState.mHeaderContentLength);
			}
		} else {
			// Ignore content-length with transfer-encoding - 2616 4.4 3
			if (Constants.LOGVV) {
				Log.v(Constants.TAG,
						"ignoring content-length because of xfer-encoding");
			}
		}
		if (Constants.LOGVV) {
			Log.v(Constants.TAG, "Content-Disposition: " +
					innerState.mHeaderContentDisposition);
			Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
			Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
			Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
			Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
			Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
		}

		boolean noSizeInfo = innerState.mHeaderContentLength == null
				&& (headerTransferEncoding == null
				|| !headerTransferEncoding.equalsIgnoreCase("chunked"));
		if (!mInfo.mNoIntegrity && noSizeInfo) {
			throw new DownloadStopedException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
					Constants.ERROR_MSG.STATUS_HTTP_DATA_ERROR);
		}
	}

	/**
	 * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
	 */
	private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
			throws DownloadStopedException, RetryDownload {
		int statusCode = response.getStatusLine().getStatusCode();
		if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
			handleServiceUnavailable(state, response);
		}
		if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
			handleRedirect(state, response, statusCode);
		}

		if (Constants.LOGV) {
			Log.i(Constants.TAG, "recevd_status = " + statusCode +
					", mContinuingDownload = " + state.mContinuingDownload);
		}
		int expectedStatus = state.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
		if (statusCode != expectedStatus) {
			handleOtherStatus(state, innerState, statusCode);
		}
	}

	/**
	 * Handle a status that we don't know how to deal with properly.
	 */
	private void handleOtherStatus(State state, InnerState innerState, int statusCode)
			throws DownloadStopedException {
		if (statusCode == 416) {
			// range request failed. it should never fail.
			throw new IllegalStateException("Http Range request failure: totalBytes = " +
					state.mTotalBytes + ", bytes recvd so far: " + state.mCurrentBytes);
		}
		int finalStatus;
		if (Downloads.Impl.isStatusError(statusCode)) {
			finalStatus = statusCode;
		} else if (statusCode >= 300 && statusCode < 400) {
			finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
		} else if (state.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
			finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
		} else {
			finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
		}
		throw new DownloadStopedException(finalStatus,
				String.format(Constants.ERROR_MSG.HTTP_CODE_ERROR, statusCode));
		//        throw new DownloadStopedException(finalStatus, "http error " +
		//                statusCode + ", mContinuingDownload: " + state.mContinuingDownload);
	}

	/**
	 * Handle a 3xx redirect status.
	 */
	private void handleRedirect(State state, HttpResponse response, int statusCode)
			throws DownloadStopedException, RetryDownload {
		if (Constants.LOGVV) {
			Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
		}
		if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
			throw new DownloadStopedException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
					Constants.ERROR_MSG.STATUS_TOO_MANY_REDIRECTS);
		}
		Header header = response.getFirstHeader("Location");
		if (header == null) {
			return;
		}
		if (Constants.LOGVV) {
			Log.v(Constants.TAG, "Location :" + header.getValue());
		}

		String newUri;
		try {
			newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
		} catch (URISyntaxException ex) {
			if (Constants.LOGV) {
				Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
						+ " for " + mInfo.mUri);
			}
			throw new DownloadStopedException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
					Constants.ERROR_MSG.STATUS_HTTP_URI_ERROR);
		}
		++state.mRedirectCount;
		state.mRequestUri = newUri;
		if (statusCode == 301 || statusCode == 303) {
			// use the new URI for all future requests (should a retry/resume be necessary)
			state.mNewUri = newUri;
		}
		throw new RetryDownload();
	}

	/**
	 * Handle a 503 Service Unavailable status by processing the Retry-After header.
	 */
	private void handleServiceUnavailable(State state, HttpResponse response)
			throws DownloadStopedException {
		if (Constants.LOGVV) {
			Log.v(Constants.TAG, "got HTTP response code 503");
		}
		state.mCountRetry = true;
		Header header = response.getFirstHeader("Retry-After");
		if (header != null) {
			try {
				if (Constants.LOGVV) {
					Log.v(Constants.TAG, "Retry-After :" + header.getValue());
				}
				state.mRetryAfter = Integer.parseInt(header.getValue());
				if (state.mRetryAfter < 0) {
					state.mRetryAfter = 0;
				} else {
					if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
						state.mRetryAfter = Constants.MIN_RETRY_AFTER;
					} else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
						state.mRetryAfter = Constants.MAX_RETRY_AFTER;
					}
					state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
					state.mRetryAfter *= 1000;
				}
			} catch (NumberFormatException ex) {
				// ignored - retryAfter stays 0 in this case.
			}
		}
		throw new DownloadStopedException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
				Constants.ERROR_MSG.STATUS_CODE_503_ERROR);
	}

	/**
	 * Send the request to the server, handling any I/O exceptions.
	 */
	private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
			throws DownloadStopedException {
		try {
			return client.execute(request);
		} catch (IllegalArgumentException ex) {
			throw new DownloadStopedException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
					Constants.ERROR_MSG.STATUS_REQUEST_ERROR, ex);
		} catch (IOException ex) {
			logNetworkState();
			throw new DownloadStopedException(getFinalStatusForHttpError(state),
					Constants.ERROR_MSG.STATUS_REQUEST_ERROR, ex);
		}
	}

	private int getFinalStatusForHttpError(State state) {
		int networkUsable = mInfo.checkCanUseNetwork();
		if (networkUsable != AppDownloadInfo.NETWORK_OK) {
			switch (networkUsable) {
			case AppDownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE:
			case AppDownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
				return Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
			case AppDownloadInfo.NETWORK_BLOCKED:
				return Downloads.Impl.STATUS_BLOCKED;
			default:
				return Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
			}
		} else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
			state.mCountRetry = true;
			return Downloads.Impl.STATUS_WAITING_TO_RETRY;
		} else {
			Log.w(Constants.TAG, "reached max retries for " + mInfo.mId);
					return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
		}
	}

	/**
	 * Prepare the destination file to receive data.  If the file already exists, we'll set up
	 * appropriately for resumption.
	 */
	private void setupDestinationFile(State state, InnerState innerState)
			throws DownloadStopedException {
		if (!TextUtils.isEmpty(
				state.mFilename)) { // only true if we've already run a thread for this download
			if (Constants.LOGV) {
				Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
						", and state.mFilename: " + state.mFilename);
			}
			if (!Helpers.isFilenameValid(state.mFilename,
					mStorageManager.getDownloadDataDirectory())) {
				// this should never happen
				throw new DownloadStopedException(Downloads.Impl.STATUS_FILE_ERROR,
						Constants.ERROR_MSG.INVALID_FILENAME);
			}
			// We're resuming a download that got interrupted
			File f = new File(state.mFilename);
			if (f.exists()) {
				if (Constants.LOGV) {
					Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
							", and state.mFilename: " + state.mFilename);
				}
				long fileLength = f.length();
				if (fileLength == 0) {
					// The download hadn't actually started, we can restart from scratch
					f.delete();
					state.mFilename = null;
					if (Constants.LOGV) {
						Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
								", BUT starting from scratch again: ");
					}
				}
				//                else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
				//                    // This should've been caught upon failure
				//                    f.delete();
				//                    throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
				//                            "Trying to resume a download that can't be resumed");
				//                }
				else {
					// All right, we'll be able to resume this download
					if (Constants.LOGV) {
						Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
								", and starting with file of length: " + fileLength);
					}
					try {
						state.mStream = new FileOutputStream(state.mFilename, true);
					} catch (FileNotFoundException exc) {
						throw new DownloadStopedException(Downloads.Impl.STATUS_FILE_ERROR,
								Constants.ERROR_MSG.FILE_NOT_EXIT, exc);
					}
					state.mCurrentBytes = (int) fileLength;
					if (mInfo.mTotalBytes != -1) {
						innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
					}
					state.mHeaderETag = mInfo.mETag;
					state.mContinuingDownload = true;
					if (Constants.LOGV) {
						Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
								", state.mCurrentBytes: " + state.mCurrentBytes +
								", and setting mContinuingDownload to true: ");
					}
				}
			}
		}

		if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
			closeDestination(state);
		}
	}

	/**
	 * Add custom headers for this download to the HTTP request.
	 */
	private void addRequestHeaders(State state, HttpGet request) {
		for (Pair<String, String> header : mInfo.getHeaders()) {
			request.addHeader(header.first, header.second);
		}

		if (state.mContinuingDownload) {
			if (state.mHeaderETag != null) {
				request.addHeader("If-Match", state.mHeaderETag);
			}
			request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
			if (Constants.LOGV) {
				Log.i(Constants.TAG, "Adding Range header: " +
						"bytes=" + state.mCurrentBytes + "-");
				Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
			}
		}
	}

	/**
	 * Stores information about the completed download, and notifies the initiating application.
	 */
	private void notifyDownloadCompleted(
			int status, boolean countRetry, int retryAfter, boolean gotData,
			String filename, String uri, String mimeType, String errorMsg) {
		LogUtils.d("DownloadThrad.java -- jiangwb", "notifyDownloadCompleted() -- \n"
				+ " status:" + status + "\n"
				+ " countRetry:" + countRetry + "\n"
				+ " retryAfter:" + retryAfter + "\n"
				+ " gotData:" + gotData + "\n"
				+ " filename:" + filename + "\n"
				+ " uri:" + uri + "\n"
				+ " mimeType:" + mimeType + "\n"
				+ " errorMsg:" + errorMsg + "\n"
				);
		notifyThroughDatabase(
				status, countRetry, retryAfter, gotData, filename, uri, mimeType,
				errorMsg);
		if (Downloads.Impl.isStatusCompleted(status)) {
			mInfo.sendIntentIfRequested();
		}
	}

	private void notifyThroughDatabase(
			int status, boolean countRetry, int retryAfter, boolean gotData,
			String filename, String uri, String mimeType, String errorMsg) {
		ContentValues values = new ContentValues();
		values.put(Downloads.Impl.COLUMN_STATUS, status);
		values.put(Downloads.Impl._DATA, filename);
		if (uri != null) {
			values.put(Downloads.Impl.COLUMN_URI, uri);
		}
		values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
		values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
		values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
		if (!countRetry) {
			values.put(Constants.FAILED_CONNECTIONS, 0);
		} else if (gotData) {
			values.put(Constants.FAILED_CONNECTIONS, 1);
		} else {
			values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
		}
		// save the error message. could be useful to developers.
		if (!TextUtils.isEmpty(errorMsg)) {
			values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
		}
		mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
	}

	/**
	 * Clean up a mimeType string so it can be used to dispatch an intent to
	 * view a downloaded asset.
	 *
	 * @param mimeType either null or one or more mime types (semi colon separated).
	 * @return null if mimeType was null. Otherwise a string which represents a
	 * single mimetype in lowercase and with surrounding whitespaces trimmed.
	 */
	private static String sanitizeMimeType(String mimeType) {
		try {
			mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);

			final int semicolonIndex = mimeType.indexOf(';');
			if (semicolonIndex != -1) {
				mimeType = mimeType.substring(0, semicolonIndex);
			}
			return mimeType;
		} catch (NullPointerException npe) {
			return null;
		}
	}

}
